这次抽空好好理解了一下java中关于oauth2的使用,及结合zuul网关实现的一些方式,稍微记录一下,省得以后忘记了
Oauth2认证流程
网上偷了一张图,显示了具体的实现方式
说实话 这张图意思还是比较明显了
登录login--> 走网关(网关放行)--> 直接到auth验证服务器 -->认证完成,把令牌放到cookie,顺便redis存一份,同时拿到客户信息
然后如果再访问其他的微服务,网关拦截,然后验证cookie中的令牌,同时跟redis中核对,并且令牌要携带上
我这里选的是JWT令牌
这里就不讲JWT的结构了 大概就是 头部载荷签名三部分 具体详情可以百度
如果想自己生成秘钥证书,可以用keytool
如果你有配置java环境变量,那么cmd直接输入(生成的文件就在你的当前文件夹下)
keytool -genkeypair -alias xhwlxhwl -keyalg RSA -keypass xhwlxhwl -keystore xhwl.jks -storepass xhwlxhwl
-alias:密钥的别名
-keyalg:使用的hash算法
-keypass:密钥的访问密码
-keystore:密钥库文件名
-storepass:密钥库的访问密码
查询证书信息:
keytool -list -keystore xhwl.jks
导出公钥
用openssl
安装 openssl:点击下载
配置openssl的path环境变量 配置到bin目录
然后再xhwl.jks的文件所在目录执行
keytool -list -rfc --keystore xhwl.jks | openssl x509 -inform pem -pubkey
输入口令,我刚才设置的是xhwlxhwl
长这样
公钥内容从 -----BEGIN PUBLIC KEY----- 到 -----END PUBLIC KEY-----
复制下来
新建一个文本 粘贴进去 把文件名字改为 public.key(最好把所有内容粘贴成一行)
这样公钥就有了
下面上代码
或者直接git(git的话有其他的微服务在里面,不太适合学习)
https://github.com/Yoki-Hua/xhiot.git
数据库新建一张表
/*
Navicat MySQL Data Transfer
Source Server : Huaz
Source Server Version : 50730
Source Host : 127.0.0.1:3306
Source Database : xhwl_user
Target Server Type : MYSQL
Target Server Version : 50730
File Encoding : 65001
Date: 2020-08-12 11:27:39
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(48) NOT NULL COMMENT '客户端ID,主要用于标识对应的应用',
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL COMMENT '客户端秘钥,BCryptPasswordEncoder加密',
`scope` varchar(256) DEFAULT NULL COMMENT '对应的范围',
`authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '认证模式',
`web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '认证后重定向地址',
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL COMMENT '令牌有效期',
`refresh_token_validity` int(11) DEFAULT NULL COMMENT '令牌刷新周期',
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details` VALUES ('xhwl', null, '$2a$10$digQWwOMjrH6VEgdJnNN2uLR2uqkjIzCOJUK0wVNZ5CmPqyrcmCxe', 'app', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost', null, '3600', '43200', null, null);
这个客户端密钥加密了 明文是xhwl
如果你打开了代码
下面直接从代码开始讲解,就不介绍那些所谓的授权码模式了
直接上密码模式,密码模式你得先有一张user表,自己建一个就行如果不想建表(测试时建议这样,不然你还得去写一个查询数据库的方法,我这里不写了,总代码里面有),就这么写,放开注释,把密码定死把下面两行注释掉
postman请求
http://localhost:9003/oauth/token
携带参数:
grant_type:密码模式授权填写password
username:账号
password:密码
然后配置Authorization
下面看登录是怎样实现的,理解登陆过程详情你就理解了oauth2了
下面只讲代码 不讲配置 配置没啥好说的 就是一些定死的东西 读出来的而已
简单的登录controller 中间调用service 路径是/oauth/login 参数是 username password
service实现类是重点,看注释吧
public AuthToken applyToken(String username, String password, String clientId, String clientSecret) {
//1.申请令牌
// 这里的意思是选择“auth-service”这个微服务的地址
// 也就是http://localhost:9003
ServiceInstance serviceInstance = loadBalancerClient.choose("auth-service");
URI uri = serviceInstance.getUri();
//那么这个url 就是 http://localhost:9003/oauth/token 是不是很眼熟?就是oauth2的获取token那个
//你也可以直接让前端调用那个接口也行 这里不是那么做的 以这里为准
String url = uri + "/oauth/token";
//就是new一个MultiValueMap 就当他是一个Map吧
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "password");
body.add("username", username);
body.add("password", password);
//里面存的参数
//这里又新建了一个Map 我也不想啊
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
//把头存进去 getHttpBasic这是个方法 在下面
headers.add("Authorization", this.getHttpBasic(clientId, clientSecret));
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
//这个不管 可以删掉 这是一个判断 加强逻辑
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
//发送请求 就相当于是你在postman上发请求 只不过变成了java来发 用Map接收
ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
//返回的值
Map map = responseEntity.getBody();
if (map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null) {
//申请令牌失败
throw new RuntimeException("申请令牌失败");
}
//2.封装结果数据,把取出来的数据存到一个对象里面去 这个对象要自己建
AuthToken authToken = new AuthToken();
authToken.setAccessToken((String) map.get("access_token"));
authToken.setRefreshToken((String) map.get("refresh_token"));
authToken.setJti((String) map.get("jti"));
//3.将jti作为redis中的key,将jwt作为redis中的value进行数据的存放 我存的是jti 对应 authtoken对象
stringRedisTemplate.boundValueOps(authToken.getJti()).set(JsonUtils.toString(authToken), ttl, TimeUnit.SECONDS);
return authToken;
}
private String getHttpBasic(String clientId, String clientSecret) {
String value = clientId + ":" + clientSecret;
byte[] encode = Base64Utils.encode(value.getBytes());
return "Basic " + new String(encode);
}
这就是整个登陆的逻辑了 对了 在controller中还要把jti加到cookie中
没看见在哪有用到密码比对?记得这个类吗 其实在这里
那里把密码写死了,只要密码是xhwl就行 账号无所谓
下面 连接到网关 通过网关来做一个拦截
由于我是zuul网关 所以自定义一个拦截器 继承zuulFilter
看注释
package com.xhwl.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.xhwl.interfaceClient.*;
import com.xhwl.config.FilterProperties;
import com.xhwl.pojo.AuthToken;
import com.xhwl.service.AuthService;
import com.xhwl.utils.CookieUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@Component
@EnableConfigurationProperties(FilterProperties.class)
public class AuthFilter extends ZuulFilter {
@Autowired
private FilterProperties filterProps;
@Autowired
private AuthService authService;
@Autowired
private AuthClient authClient;
@Value("${xh.cookieDomain}")
private String cookieDomain;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.FORM_BODY_WRAPPER_FILTER_ORDER - 1;
}
/**
* true过滤器生效
* false过滤器不生效
*
*
*/
@Override
public boolean shouldFilter() {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
//获取请求资源地址
String requestURI = request.getRequestURI();
//获取白名单 这些配置在application中
List<String> allowPaths = filterProps.getAllowPaths();
//如果资源白名单,出现在资源地址中,则放行,不是,则拦截
for (String allowPath : allowPaths) {
if (requestURI.startsWith(allowPath)){
return false;
}
}
return true;
}
/**
* 校验登录状态,从cookie中获取jti的值
* 从redis中获取token 插入header中
* @return
* @throws
*/
@Override
public Object run() {
//拦截后 初始化一个RequestContext
RequestContext currentContext = null;
currentContext = RequestContext.getCurrentContext();
//获取request和response
HttpServletRequest request = currentContext.getRequest();
HttpServletResponse response = currentContext.getResponse();
//由于我们存的是jti 所以取jti
//2.从cookie中获取jti的值,如果该值不存在,说明没有token ,拒绝本次访问
String jti = authService.getJtiFromCookie(request);
if (StringUtils.isEmpty(jti)){
currentContext.setResponseStatusCode(HttpServletResponse.SC_UNAUTHORIZED);
currentContext.setSendZuulResponse(false);
currentContext.setResponseBody("NO TOKEN!!!");
return null;
}
//因为我们往redis也存了一份 所以要通过cookie取出来的jti取redis查有没有对应的token
//3.从redis中获取jwt的值,如果该值不存在,说明已过期,重新刷新token
String jwt = authService.getJwtTokenFromRedis(jti);
if (StringUtils.isEmpty(jwt)) {
currentContext.setResponseStatusCode(HttpServletResponse.SC_UNAUTHORIZED);
currentContext.setSendZuulResponse(false);
currentContext.setResponseBody("TOKEN INVALID!!!");
return null;
} else {
//就算有token 也要判断token还剩多久的有效期
Long jwtTime = authService.getJwtTimeFromRedis(jti);
//如果jwtTime时间<30分钟(1800秒),刷新,重新拿个新的token,大于不管
if (jwtTime < 1800) {
String refreshToken = authService.getJwtRefreshTokenFromRedis(jti);
ResponseEntity<AuthToken> token = authClient.refreshToken("refresh_token", refreshToken, jti);
jwt = token.getBody().getAccessToken();
//更新浏览器的cookie
CookieUtil.addCookie(response,cookieDomain,"/","uid",token.getBody().getJti(),-1,false);
}
}
//4.header携带令牌的信息 继续传递到微服务 微服务才能通过验证查询
currentContext.addZuulRequestHeader("Authorization","Bearer "+jwt);
return null;
}
}
这么做其实相对来说比较麻烦 但是保证了token安全性,因为token一旦颁发,没有过期之前,都是有效的,哪怕你刷新了token 旧的token还是有效,
我们用redis做二次校验,刷新token后原来的token会从redis删除掉,这样每一次来的请求都会先去看redis里是否存在 所以旧的token是找不到的,从网关走就无法走通
emm....感觉讲的不是很清楚,但是代码已经贴上了 看源码吧 反正我算是跑通了的 可能代码写的很垃圾,但是功能实现了(狗头保命)
OVER!